﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Shapes;

namespace RoofusUIControls
{
    /// <summary>
    /// Allows one to drag/connect from one item to the next. Will draw lines between the connected things.
    /// </summary>
    public class DragConnectorCanvas : Canvas
    {
        /// <summary>
        /// Attached property indicating the control is a "source"
        /// </summary>
        public static readonly DependencyProperty IsSourceProperty;

        /// <summary>
        /// Gets the IsSource property value of a particular item.
        /// </summary>
        /// <param name="uiElement"></param>
        /// <returns></returns>
        public static bool GetIsSource(UIElement uiElement)
        {
            if (uiElement == null)
                return false;

            return (bool)uiElement.GetValue(IsSourceProperty);
        }

        /// <summary>
        /// Sets the IsSource property of a particular control.
        /// </summary>
        /// <param name="uiElement"></param>
        /// <param name="value"></param>
        public static void SetIsSource(UIElement uiElement, bool value)
        {
            if (uiElement != null)
                uiElement.SetValue(IsSourceProperty, value);
        }

        /// <summary>
        /// Attached property indicating the control is a "sink"
        /// </summary>
        public static readonly DependencyProperty IsSinkProperty;

        /// <summary>
        /// Gets the IsSink property value of a particular item.
        /// </summary>
        /// <param name="uiElement"></param>
        /// <returns></returns>
        public static bool GetIsSink(UIElement uiElement)
        {
            if (uiElement == null)
                return false;

            return (bool)uiElement.GetValue(IsSinkProperty);
        }

        /// <summary>
        /// Sets the IsSink property of a particular control.
        /// </summary>
        /// <param name="uiElement"></param>
        /// <param name="value"></param>
        public static void SetIsSink(UIElement uiElement, bool value)
        {
            if (uiElement != null)
                uiElement.SetValue(IsSinkProperty, value);
        }

        /// <summary>
        /// Attached property indicating the control is a "source"
        /// </summary>
        public static readonly DependencyProperty SourceSinkObjectProperty;

        /// <summary>
        /// Gets the IsSource property value of a particular item.
        /// </summary>
        /// <param name="uiElement"></param>
        /// <returns></returns>
        public static object GetSourceSinkObject(UIElement uiElement)
        {
            if (uiElement == null)
                return null;

            return uiElement.GetValue(SourceSinkObjectProperty);
        }

        /// <summary>
        /// Sets the IsSource property of a particular control.
        /// </summary>
        /// <param name="uiElement"></param>
        /// <param name="value"></param>
        public static void SetSourceSinkObject(UIElement uiElement, object value)
        {
            if (uiElement != null)
                uiElement.SetValue(SourceSinkObjectProperty, value);
        }

        /// <summary>
        /// Static ctor - used to declare attached and dependent properties
        /// </summary>
        static DragConnectorCanvas()
        {
            IsSinkProperty = DependencyProperty.RegisterAttached(
                "IsSink",
                typeof(bool),
                typeof(DragConnectorCanvas),
                new UIPropertyMetadata(false));
            IsSourceProperty = DependencyProperty.RegisterAttached(
                "IsSource",
                typeof(bool),
                typeof(DragConnectorCanvas),
                new UIPropertyMetadata(false));
            SourceSinkObjectProperty = DependencyProperty.RegisterAttached(
                "SourceSinkObject",
                typeof(object),
                typeof(DragConnectorCanvas),
                new UIPropertyMetadata(null));
        }

        /// <summary>
        /// Keep track of where the start location is for the line we
        /// are drawing!
        /// </summary>
        private UIElement _start_loc;

        /// <summary>
        /// Keeps track of the line getting drawn.
        /// </summary>
        private Line _drawing_line = null;

        /// <summary>
        /// Place where the mouse went down.
        /// </summary>
        private Point _mouse_down_at;

        /// <summary>
        /// Lock onto mouse and track the drawing for this one guy.
        /// </summary>
        private UIElement ElementStartLocation
        {
            get
            {
                return _start_loc;
            }
            set
            {
                if (_start_loc != null)
                {
                    _start_loc.ReleaseMouseCapture();
                    _start_loc = null;
                    if (_drawing_line != null)
                    {
                        Children.Remove(_drawing_line);
                        _drawing_line = null;
                    }
                }
                _start_loc = value;
                if (_start_loc != null)
                {
                    _start_loc.CaptureMouse();
                }
            }
        }

        /// <summary>
        /// Are they trying to start a draw operation? Walk the visual tree and see if we can
        /// find someone with the IsSource property set.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnMouseLeftButtonDown(System.Windows.Input.MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            if (e.Handled)
            {
                return;
            }

            ///
            /// Is this a good item to look at?
            /// 

            UIElement elem = FindSourceElement(e.OriginalSource as DependencyObject);
            if (elem != null)
            {
                ElementStartLocation = elem;
                _mouse_down_at = e.GetPosition(this);
            }
            e.Handled = true;
        }


        /// <summary>
        /// Draw lines if we are doing a connection.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnMouseMove(System.Windows.Input.MouseEventArgs e)
        {
            base.OnMouseMove(e);
            if (ElementStartLocation == null)
            {
                return;
            }

            if (_drawing_line == null)
            {
                _drawing_line = new Line();
                _drawing_line.X1 = _mouse_down_at.X;
                _drawing_line.Y1 = _mouse_down_at.Y;
                _drawing_line.Stroke = Brushes.Blue;
                Children.Add(_drawing_line);
            }
            Point current = e.GetPosition(this);
            _drawing_line.X2 = current.X;
            _drawing_line.Y2 = current.Y;
        }

        /// <summary>
        /// They have released the line. We should let folks know!
        /// </summary>
        /// <param name="e"></param>
        protected override void OnMouseUp(System.Windows.Input.MouseButtonEventArgs e)
        {
            base.OnMouseUp(e);
            if (ElementStartLocation == null)
            {
                return;
            }

            ///
            /// Did they end on something worth while?
            /// 

            UIElement start_element = ElementStartLocation;
            ElementStartLocation = null;
            IInputElement actual_hit = InputHitTest(e.GetPosition(this));
            UIElement final_sink = FindSinkElement(actual_hit as DependencyObject);
            if (final_sink != null)
            {
                OnDragConnectionMade(start_element, final_sink);
            }

            ///
            /// Reset ourselves.
            /// 

            ElementStartLocation = null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="start_element"></param>
        /// <param name="end_element"></param>
        public delegate void DragConnectionMadeCallback(object start_element, object end_element);

        /// <summary>
        /// Fires when the user makes a new drawing connection.
        /// </summary>
        public event DragConnectionMadeCallback DragConnectionMade;

        /// <summary>
        /// Fired when a connection is made. Translate them to their representitive objects, if we know anything
        /// about them.
        /// </summary>
        /// <param name="ElementStartLocation"></param>
        /// <param name="final_sink"></param>
        private void OnDragConnectionMade(UIElement start, UIElement final_sink)
        {
            object f_start = start;
            object f_sink = final_sink;
            if (DragConnectionMade != null)
            {
                if (GetSourceSinkObject(start) != null)
                {
                    f_start = GetSourceSinkObject(start);
                }
                if (GetSourceSinkObject(final_sink) != null)
                {
                    f_sink = GetSourceSinkObject(final_sink);
                }
                DragConnectionMade(f_start, f_sink);
            }
        }

        private delegate bool elementTypeDelegate(UIElement e);

        /// <summary>
        /// Starting with p, walk up the visual tree to see if anyone is marked as a Source. If they are, return
        /// them!
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        private UIElement FindElement(DependencyObject p, elementTypeDelegate test)
        {
            while (p != null)
            {
                UIElement e = p as UIElement;
                if (e != null && test(e))
                {
                    break;
                }

                /// Move up the tree. Stolen code from DragCanvas.
                if (p is Visual || p is Visual3D)
                {
                    p = VisualTreeHelper.GetParent(p);
                }
                else
                {
                    p = LogicalTreeHelper.GetParent(p);
                }
            }

            return p as UIElement;
        }

        /// <summary>
        /// Find an element in the visual hierarchy that is a source.
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        private UIElement FindSourceElement(DependencyObject p)
        {
            return FindElement(p, x => GetIsSource(x));
        }

        /// <summary>
        /// Find an element in the visual hierarchy that is a sink.
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        private UIElement FindSinkElement(DependencyObject p)
        {
            return FindElement(p, x => GetIsSink(x));
        }

        /// <summary>
        /// We should look at all of our children, and return a true size that we need to display. If there is a constraint,
        /// we will just lop off a bit.
        /// </summary>
        /// <param name="constraint"></param>
        /// <returns></returns>
        protected override Size MeasureOverride(Size constraint)
        {
            Size howBigWeAre = new Size();
            foreach (UIElement ele in InternalChildren)
            {
                ele.Measure(constraint);
                Point position = new Point(Canvas.GetLeft(ele), Canvas.GetTop(ele));
                Vector bottomleft = (Vector)position + new Vector(ele.DesiredSize.Width, ele.DesiredSize.Height);
                if (bottomleft.X > howBigWeAre.Width)
                {
                    howBigWeAre.Width = bottomleft.X;
                }
                if (bottomleft.Y > howBigWeAre.Height)
                {
                    howBigWeAre.Height = bottomleft.Y;
                }
            }

            ///
            /// Are we too big for our britches?
            ///

            if (constraint.Height < howBigWeAre.Height)
            {
                howBigWeAre.Height = constraint.Height;
            }
            if (constraint.Width < howBigWeAre.Width)
            {
                howBigWeAre.Width = constraint.Width;
            }

            return howBigWeAre;
        }
    }
}
